namespace CcNetTeamBuildTask
{
    using System;
    using System.Collections;
    using System.IO;
    using System.Text;
    using System.Threading;
    using System.Xml.Serialization;
    using System.Web.Services;

    using ThoughtWorks.CruiseControl.Core.Util;

    using Microsoft.TeamFoundation.Build.Common;
    using MsBuildProject = Microsoft.TeamFoundation.Build.Common.Project;
    using Proxy = Microsoft.TeamFoundation.Build.Proxy;
    using Microsoft.TeamFoundation.VersionControl.Client;
    using Microsoft.TeamFoundation.VersionControl.Common;
    using Microsoft.TeamFoundation.Client;

    public class TeamBuildManager
    {
        private const string defaultPathToTfExe = @"C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\tf.exe";
        private const string teamBuildRspFilename = "TfsBuild.rsp";
        private const string teamBuildInProgressErrorCode = "TF42045";
        
        private string _teamBuildType;
        private string _teamProject;
        private string _teamBuildServer;
        private string _tfsServerUri;
        private string _workspace;
        private IList _msBuildParameters;
        private string _msTestResultsPath;
        private string _buildDropPath;
        private string _tfsBuildNumber;
        
        //private string _buildServer;
        private string _pathToTfExe = defaultPathToTfExe;


        #region Public Properties
        /// <summary>
        /// The Team Build Type to be run.
        /// </summary>
        public string TeamBuildType
        {
            get { return _teamBuildType; }
            set { _teamBuildType = value; }
        }

        /// <summary>
        /// The TFS Team Project that contains the Team Build Type to be run.
        /// </summary>
        public string TeamProject
        {
            get { return _teamProject; }
            set { _teamProject = value; }
        }

        /// <summary>
        /// The Team Build Server to run the build on.
        /// </summary>
        public string TeamBuildServer
        {
            get { return _teamBuildServer; }
            set { _teamBuildServer = value; }
        }

        /// <summary>
        /// The full URL of the TFS Server.
        /// </summary>
        public string TfsServerUri
        {
            get { return _tfsServerUri; }
            set { _tfsServerUri = value; }
        }

        /// <summary>
        /// The TFS workspace to be used when polling for changes.
        /// </summary>
        public string Workspace
        {
            get { return _workspace; }
            set { _workspace = value; }
        }

        /// <summary>
        /// The path to the TFSBuild.rsp file that contains custom MSBuild properties for a given Team Build.
        /// </summary>
        public string TeamBuildRspFilename
        {
            get { return teamBuildRspFilename; }
        }

        /// <summary>
        /// A collection of MSBuild parameters to be written to the TFSBuild.rsp file prior to starting the Team Build Type.
        /// </summary>
        public IList MsBuildParameters
        {
            get
            {
                if (_msBuildParameters == null)
                {
                    _msBuildParameters = new ArrayList();
                }
                return _msBuildParameters;
            }
            set { _msBuildParameters = value; }
        }

        /// <summary>
        /// If true, CCNet will attempt to gather MSTest Code Coverage results from the Team Build.
        /// </summary>
        public string MsTestResultsPath
        {
            get { return _msTestResultsPath; }
        }

        /// <summary>
        /// Specifies the full path to TF.EXE, defaults to 'C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\tf.exe'
        /// </summary>
        public string PathToTfExe
        {
            get { return _pathToTfExe; }
            set { _pathToTfExe = value; }
        }

        /// <summary>
        /// Reports the build's Drop location on the build server.
        /// </summary>
        public string BuildDropPath
        {
            get { return _buildDropPath; }
        }
        
        /// <summary>
        /// Reports the build number used by TFS to identify the build
        /// </summary>
        public string TfsBuildNumber
        {
            get { return _tfsBuildNumber; }
        }

        #endregion


        /// <summary>
        /// triggers a team foundation server team build
        /// </summary>
        public BuildConstants.BuildStatusIconID PerformTeamBuild()
        {
            BuildConstants.BuildStatusIconID _teamBuildStatus;
            const int ThreadSleepInterval = 10000; // polling interval for detecting completed Team Builds - 10 seconds.

            Log.Info("Building '" + _teamBuildType +
                "' for Team Project '" + _teamProject +
                "' on " + _tfsServerUri +
                (String.IsNullOrEmpty(_teamBuildServer) ? "" : " on BuildMachine: " + _teamBuildServer));

            // connect to TFS
            TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer(_tfsServerUri);
            VersionControlServer sc = (VersionControlServer)tfs.GetService(typeof(VersionControlServer));

            // ensure we have a local copy of the TeamBuild folder contents
            String serverPath = VersionControlPath.Combine(VersionControlPath.RootFolder, _teamProject);
            serverPath = VersionControlPath.Combine(serverPath, "TeamBuildTypes");
            serverPath = VersionControlPath.Combine(serverPath, _teamBuildType);

            // form the project file server path
            serverPath = VersionControlPath.Combine(serverPath, "TfsBuild.proj");

            // form the project file local path
            String localPath = System.Environment.GetEnvironmentVariable("TEMP");
            localPath = Path.Combine(localPath, "TeamBuildTypes");
            localPath = Path.Combine(localPath, _teamBuildType);
            if (!Directory.Exists(localPath))
            {
                Directory.CreateDirectory(localPath);
            }
            localPath = Path.Combine(localPath, "TfsBuild.proj");

            // download the project file from server path to local path
            Log.Debug("Downloading TfsBuild.proj from server...");
            sc.DownloadFile(serverPath, localPath);

            // deserialize the project file into a project object
            MsBuildProject projectInfo = ReadBuildTypeInfo(localPath);

            // define a new build 
            Proxy.BuildController controller = (Proxy.BuildController)tfs.GetService(typeof(Proxy.BuildController));
            Proxy.BuildStore store = (Proxy.BuildStore)tfs.GetService(typeof(Proxy.BuildStore));
            Proxy.BuildParameters buildParams = new Proxy.BuildParameters();

            // set new build parameters
            buildParams.TeamFoundationServer = _tfsServerUri;
            buildParams.TeamProject = _teamProject;
            buildParams.BuildType = _teamBuildType;
            buildParams.BuildDirectory = projectInfo.PropertyGroup.BuildDirectoryPath;
            buildParams.BuildMachine = String.IsNullOrEmpty(_teamBuildServer) ?
                projectInfo.ProjectExtensions.BuildMachine : _teamBuildServer;

            // kick off the new build
            Log.Info("Calling TeamBuild web service...");
            string buildUri = controller.StartBuild(buildParams);
            Log.Info(string.Format("BuildUri of new TeamBuild: {0}", buildUri));

            // keep checking until the build completes (or times out)
            Proxy.BuildData bd;
            bool buildComplete = false;
            do
            {
                try
                {
                    Thread.Sleep(ThreadSleepInterval);
                }
                catch (Exception)
                {
                }

                // Get the build details
                bd = store.GetBuildDetails(buildUri);

                // Save the status of the build
                _teamBuildStatus = (BuildConstants.BuildStatusIconID)bd.BuildStatusId;
                buildComplete = (
                    _teamBuildStatus == BuildConstants.BuildStatusIconID.BuildSucceeded ||
                    _teamBuildStatus == BuildConstants.BuildStatusIconID.BuildFailed ||
                    _teamBuildStatus == BuildConstants.BuildStatusIconID.BuildStopped)
                    && (bd.FinishTime.Year > 1);

            } while (!buildComplete);


            // Derive the path to any test results from the build's drop location
            if (bd != null)
            {
                _buildDropPath = bd.DropLocation;
                _msTestResultsPath = Path.Combine(_buildDropPath, "TestResults");
                
                // Get the TFS Build Number, so this is available CCnet if need be
                _tfsBuildNumber = bd.BuildNumber;
            }


            return _teamBuildStatus;
        }

        /// <summary>
        /// reads the build type info from a file and casts it as a Project
        /// </summary>
        private MsBuildProject ReadBuildTypeInfo(string filename)
        {
            XmlSerializer projectSerializer = new XmlSerializer(typeof(MsBuildProject));

            using (FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read))
            {
                return ((MsBuildProject)(projectSerializer.Deserialize(stream)));
            }

        }

        /// <summary>
        /// takes the supplied msbuild parameters and adds them to the TfSBuild.rsp file that
        /// forms part of the team build system
        /// </summary>
        public void UpdateTfsBuildRspFile()
        {
            // Set the path to TF.EXE using either the default or one specified in config
            string pathToTfExe = String.IsNullOrEmpty(_pathToTfExe) ? defaultPathToTfExe : _pathToTfExe;

            // form the TFS source control path to the TeamBuild area
            string teamBuildSccPath = "$/" + _teamProject + "/TeamBuildTypes/" + _teamBuildType;

            // Derive the path to the current TeamBuild in source control
            Log.Debug("Deriving workspace path for " + teamBuildSccPath + " in workspace " + _workspace + ".");
            string teamBuildPath = getTFSWorkspaceDirectory(teamBuildSccPath,
                _tfsServerUri,
                this.Workspace,
                pathToTfExe);

            Log.Debug("Derived Workspace path: " + teamBuildPath);

            // ensure the local path exists (i.e. cater for first-time execution)
            if (!Directory.Exists(teamBuildPath))
            {
                Directory.CreateDirectory(teamBuildPath);
            }

            // get latest of the TeamBuild area
            int ret = Utilities.ShellExecute(pathToTfExe,
                "get \"" + teamBuildSccPath + "\" /recursive",
                teamBuildPath);

            // Form the path to the .rsp file within the above TeamBuild area
            string rspFilename = Path.Combine(teamBuildPath, teamBuildRspFilename);
            string rspSccPath = teamBuildSccPath + "/TfsBuild.rsp";

            string tfCmd = "checkout \"" + rspSccPath + "\" /lock:checkout";
            Log.Debug("Attempting check-out: " + tfCmd);
            ret = Utilities.ShellExecute(pathToTfExe,
                tfCmd,
                Path.GetDirectoryName(rspFilename));

            if (ret != 0)
            {
                Log.Warning("Unable to complete the check-out operation. Error: " + ret);
            }


            // ensure the file is not read only
            if (File.Exists(rspFilename))
            {
                if ((File.GetAttributes(rspFilename) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
                {
                    File.SetAttributes(rspFilename, FileAttributes.Archive);
                }
            }

            // write the custom parameters to TFSBuild.rsp
            using (StreamWriter sw = new StreamWriter(rspFilename))
            {
                foreach (object buildParameter in this._msBuildParameters)
                {
                    sw.WriteLine(buildParameter.ToString());
                }
            }

            Log.Debug("New Contents of " + rspFilename + ":\n");
            using (TextReader tr = File.OpenText(rspFilename))
            {
                Log.Debug(tr.ReadToEnd());
            }

            // Check-in updated .rsp file
            tfCmd = "checkin TfsBuild.rsp /noprompt /comment:\"Updated by CCNet TeamBuild Task\"";
            Log.Debug("Attempting check-in: " + tfCmd);
            ret = Utilities.ShellExecute(pathToTfExe,
                tfCmd,
                Path.GetDirectoryName(rspFilename));

            if (ret != 0)
            {
                Log.Warning("Unable to complete the check-in operation. Error: " + ret);
            }


        }

        /// <summary>
        /// takes the specified TFS server path and derives its physical workspace mapping
        /// </summary>
        private string getTFSWorkspaceDirectory(string tfsPath, string tfsServer, string workspace, string pathToTfExe)
        {
            string tfsWorkspacePath = string.Empty;
            string processOutput;
            try
            {
                using (System.Diagnostics.Process p = new System.Diagnostics.Process())
                {
                    // Redirect the output stream of the child process.
                    p.StartInfo.UseShellExecute = false;
                    p.StartInfo.RedirectStandardOutput = true;
                    p.StartInfo.RedirectStandardError = true;

                    //run the command 
                    //tf.exe workfold $/
                    p.StartInfo.FileName = pathToTfExe;
                    p.StartInfo.Arguments = " workfold \"" + tfsPath + "\" /server:" + tfsServer + " /workspace:" + workspace;
                    p.Start();

                    // Read the output stream first and then wait.
                    processOutput = p.StandardOutput.ReadToEnd(); //N.B. this line can take up to a minute to execute
                    string errorMessage = p.StandardError.ReadToEnd();

                    p.WaitForExit();

                    //manipulate the string
                    //to get the directory out
                    if (p.ExitCode != 0)
                    {
                        //assume error has occurred and return
                        Log.Error("TF.exe error occurred\n\n" + errorMessage);
                    }
                }

                //output should be of the form
                //C:\>tf workfold $/
                //==================================================================
                //Workspace: MyLaptop (MyUser)
                //Server   : http://MyTfsServer:8080/
                // $/: C:\Data\MyUser\TFSRoot

                string workfoldMatchString = tfsPath + ": ";
                if (processOutput != null
                    && processOutput.IndexOf(workfoldMatchString) > -1)
                {
                    //search for $/: and trim the rest of the string
                    tfsWorkspacePath = processOutput.Substring(processOutput.IndexOf(workfoldMatchString) + workfoldMatchString.Length).Trim();
                }
            }
            catch (Exception ex)
            {
                Log.Error("Failed to retrieve TFS workspace path\n" + ex.Message);
            }

            return tfsWorkspacePath;
        }

    }
}
